<script lang="ts">
    import { page } from "$app/stores";
    import type { Trail } from "$lib/models/trail";
    import { theme } from "$lib/stores/theme_store";
    import { findStartAndEndPoints } from "$lib/util/geojson_util";
    import { toGeoJson } from "$lib/util/gpx_util";
    import {
        createMarkerFromWaypoint,
        createPopupFromTrail,
        FontawesomeMarker,
    } from "$lib/util/maplibre_util";
    import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control";
    import { StyleSwitcherControl } from "$lib/vendor/maplibre-style-switcher/style-switcher-control";
    import type { GeoJSON } from "geojson";
    import * as M from "maplibre-gl";
    import "maplibre-gl/dist/maplibre-gl.css";
    import { createEventDispatcher, onDestroy, onMount } from "svelte";
    import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule";
    import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control";
    import type { Settings } from "$lib/models/settings";
    import directionCaret from "$lib/assets/svgs/caret-right-solid.svg";

    export let trails: Trail[] = [];
    export let markers: M.Marker[] = [];
    export let map: M.Map | null = null;
    export let drawing: boolean = false;
    export let showElevation: boolean = true;
    export let showInfoPopup: boolean = false;
    export let showGrid: boolean = false;
    export let showStyleSwitcher: boolean = true;
    export let showFullscreen: boolean = false;
    export let showTerrain: boolean = false;
    export let fitBounds: "animate" | "instant" | "off" = "instant";
    export let onMarkerDragEnd:
        | ((marker: M.Marker, wpId?: string) => void)
        | undefined = undefined;

    export let elevationProfileContainer: string | HTMLDivElement | undefined =
        undefined;
    export let mapOptions: Partial<M.MapOptions> | undefined = undefined;

    export let activeTrail: number = 0;
    export let minZoom: number = 0;

    let mapContainer: HTMLDivElement;
    let epc: ElevationProfileControl | null = null;
    let graticule: MaplibreGraticule | null = null;

    let layers: Record<
        string,
        {
            startMarker: M.Marker | null;
            endMarker: M.Marker | null;
            source: M.GeoJSONSource | null;
            layer: M.LineLayerSpecification | null;
            listener: {
                onEnter: ((e: M.MapMouseEvent) => void) | null;
                onLeave: ((e: M.MapMouseEvent) => void) | null;
                onClick: ((e: M.MapMouseEvent) => void) | null;
            };
        }
    > = {};

    const dispatch = createEventDispatcher();

    const trailColors = [
        "#3549BB",
        "#592E9E",
        "#47A2CD",
        "#62D5BC",
        "#7DDE95",
        "#759E2E",
        "#BBA535",
        "#CD6F47",
        "#D5627B",
        "#DE7DC5",
    ];

    $: data = getData(trails);

    $: if (data && map) {
        initMap();
    }

    $: if ($theme == "dark") {
        epc?.toggleTheme({
            profileBackgroundColor: "#191b24",
            elevationGridColor: "#ddd2",
            labelColor: "#ddd8",
            crosshairColor: "#fff5",
        });
    } else {
        epc?.toggleTheme({
            profileBackgroundColor: "#242734",
            elevationGridColor: "#0002",
            labelColor: "#0009",
            crosshairColor: "#0005",
        });
    }

    $: if (drawing && map) {
        map.getCanvas().style.cursor = "crosshair";
    } else if (!drawing && map) {
        map.getCanvas().style.cursor = "inherit";
        addStartEndMarkers(
            trails[activeTrail],
            trails[activeTrail]?.id ?? activeTrail.toString(),
            data?.at(activeTrail),
        );
    }

    $: if (showGrid) {
        if (!graticule) {
            graticule = new MaplibreGraticule({
                minZoom: 0,
                maxZoom: 20,
                showLabels: true,
                labelType: "hdms",
                labelSize: 10,
                labelColor: "#858585",
                longitudePosition: "top",
                latitudePosition: "right",
                paint: {
                    "line-opacity": 0.8,
                    "line-color": "rgba(0,0,0,0.2)",
                },
            });
        }
        map?.addControl(graticule);
    } else {
        if (graticule) {
            map?.removeControl(graticule);
        }
    }

    function getData(trails: Trail[]) {
        if (!trails.length) {
            return [];
        }

        const r: GeoJSON[] = [];
        trails.forEach((t) => {
            if (t.expand.gpx_data) {
                r.push(toGeoJson(t.expand.gpx_data) as GeoJSON);
            } else if (t.lat && t.lon) {
                r.push({
                    id: "",
                    type: "Feature",
                    properties: {},
                    geometry: {
                        type: "Point",
                        coordinates: [t.lon ?? 0, t.lat ?? 0],
                    },
                } as GeoJSON);
            }
        });
        return r;
    }

    function initMap() {
        if (!map) {
            return;
        }

        if (data[activeTrail] && showElevation) {
            epc?.setData(
                data[activeTrail]!,
                trails.at(activeTrail)!.expand.waypoints,
            );
            epc?.showProfile();
        }

        trails.forEach((t, i) => {
            const layerId = t.id ?? i.toString();
            addTrailLayer(t, layerId, i, data[i]);
        });

        Object.keys(layers).forEach((layerId) => {
            const isStillVisible = trails.some((t) => t.id === layerId);
            if (!isStillVisible) {
                removeTrailLayer(layerId);
            }
        });

        if (
            !drawing &&
            fitBounds !== "off" &&
            data.some((d) => d.bbox !== undefined)
        ) {
            flyToBounds(fitBounds == "animate");
        }
    }

    function getBounds() {
        let minX = Infinity,
            minY = Infinity,
            maxX = -Infinity,
            maxY = -Infinity;

        for (const [xMin, yMin, xMax, yMax] of data
            .filter((d) => d.bbox !== undefined)
            .map((d) => d.bbox!)) {
            minX = Math.min(minX, xMin);
            minY = Math.min(minY, yMin);
            maxX = Math.max(maxX, xMax);
            maxY = Math.max(maxY, yMax);
        }

        return new M.LngLatBounds([minX, minY, maxX, maxY]);
    }

    function flyToBounds(animate: boolean = true) {
        const bounds = data[activeTrail]
            ? (data[activeTrail].bbox as M.LngLatBoundsLike)
            : getBounds();

        if (!bounds) {
            return;
        }

        map!.fitBounds(bounds, {
            animate: animate,
            padding: {
                top: 16,
                left: 16,
                right: 16,
                bottom:
                    16 +
                    (epc?.isProfileShown
                        ? map!.getContainer().clientHeight * 0.3
                        : 0),
            },
        });
    }

    function removeTrailLayer(id: string) {
        if (!layers[id]) {
            return;
        }
        const layer = layers[id];

        if (layer.layer) {
            map?.removeLayer(id);
        }
        if (layer.source) {
            map?.removeSource(id);
        }
        layer.startMarker?.remove();
        layer.endMarker?.remove();

        map?.off("mouseenter", id, layers[id].listener.onEnter!);
        map?.off("mouseleave", id, layers[id].listener.onLeave!);
        map?.off("click", id, layers[id].listener.onClick!);

        delete layers[id];
    }

    function createEmptyLayer(id: string) {
        if (!layers[id]) {
            layers[id] = {
                startMarker: null,
                endMarker: null,
                source: null,
                layer: null,
                listener: {
                    onClick: null,
                    onEnter: null,
                    onLeave: null,
                },
            };
        }
    }

    function addTrailLayer(
        trail: Trail,
        id: string,
        index: number,
        geojson: GeoJSON | null | undefined,
    ) {
        if (!geojson || !map) {
            return;
        }

        createEmptyLayer(id);

        if (!layers[id].source) {
            try {
                map.addSource(id, {
                    type: "geojson",
                    data: geojson,
                });
                layers[id].source = map.getSource(id) as M.GeoJSONSource;
                // map.addSource("trail-source", {
                //     type: "vector",
                //     url: `http://localhost:8080/data/out.json`,
                // });
            } catch (e) {
                return;
            }
        } else {
            layers[id].source.setData(geojson);
        }

        if (!layers[id].layer) {
            map.addLayer({
                id: id,
                type: "line",
                source: id,
                minzoom: minZoom,
                paint: {
                    "line-color": trailColors[index % trailColors.length],
                    "line-width": 5,
                },
            });
            layers[id].layer = map.getLayer(id) as M.LineLayerSpecification;
            layers[id].listener.onEnter = (e) => highlightTrail(id);
            layers[id].listener.onLeave = (e) => unHighlightTrail(id);
            layers[id].listener.onClick = (e) => focusTrail(trail, e);

            map.on("mouseenter", id, layers[id].listener.onEnter);
            map.on("mouseleave", id, layers[id].listener.onLeave);
            map.on("click", id, layers[id].listener.onClick);

            // map.addLayer({
            //     id: "trail-layer",
            //     type: "line",
            //     source: "trail-source",
            //     "source-layer": "Herzogstand", // Replace with the actual layer name in the .mbtiles file
            //     layout: {
            //         "line-join": "round",
            //         "line-cap": "round",
            //     },
            //     paint: {
            //         "line-color": "#648ad5",
            //         "line-width": 5,
            //     },
            // });
        }

        if (!drawing) {
            addStartEndMarkers(trail, id, geojson);
        }
    }

    function addCaretLayer(id?: string) {
        if (!map || !id) {
            return;
        }
        map.addLayer({
            id: "direction-carets",
            type: "symbol",
            source: id,
            layout: {
                "symbol-placement": "line",
                "symbol-spacing": [
                    "interpolate",
                    ["exponential", 1.5],
                    ["zoom"],
                    minZoom,
                    80,
                    18,
                    200,
                ],
                "icon-image": "direction-caret",
                "icon-size": [
                    "interpolate",
                    ["exponential", 1.5],
                    ["zoom"],
                    minZoom,
                    0.5,
                    18,
                    0.8,
                ],
            },
        });
    }

    function removeCaretLayer() {
        if (!map || !map.getLayer("direction-carets")) {
            return;
        }
        map.removeLayer("direction-carets");
    }

    export function highlightTrail(id: string) {
        map?.setPaintProperty(id, "line-width", 7);
        // map?.setPaintProperty(id, "line-color", "#2766e3");
    }

    export function unHighlightTrail(id: string) {
        map?.setPaintProperty(id, "line-width", 5);
        // map?.setPaintProperty(id, "line-color", "#648ad5");
    }

    export function focusTrail(trail: Trail, e?: M.MapMouseEvent) {
        const currentlyFocussedTrail = trails[activeTrail];
        if (currentlyFocussedTrail) {
            unFocusTrail(currentlyFocussedTrail);
        }
        e?.preventDefault();
        dispatch("select", trail);
        const index = trails.findIndex((t) => t.id == trail.id);
        if (index == -1) {
            return;
        }
        activeTrail = index;

        highlightTrail(trail.id!);
        if (data[activeTrail] && showElevation) {
            epc?.setData(
                data[activeTrail]!,
                trails.at(activeTrail)!.expand.waypoints,
            );
            epc?.showProfile();
        }
        showWaypoints();
        addCaretLayer(trail.id!);
        flyToBounds();
    }

    export function unFocusTrail(trail: Trail) {
        dispatch("unselect", trail);
        activeTrail = -1;
        unHighlightTrail(trail.id!);
        flyToBounds();

        if (showElevation) {
            epc?.hideProfile();
        }
        hideWaypoints();
        removeCaretLayer();
    }

    function addStartEndMarkers(
        trail: Trail,
        id: string,
        geojson: GeoJSON | null | undefined,
    ) {
        if (!map || !trail) {
            return;
        }
        createEmptyLayer(id);

        layers[id].startMarker ??= new FontawesomeMarker(
            { icon: "fa fa-bullseye" },
            {},
        );

        if (!geojson) {
            if (trail.lon && trail.lat) {
                layers[id].startMarker
                    .setLngLat([trail.lon, trail.lat])
                    .addTo(map);
            }
            return;
        }

        const startEndPoint = findStartAndEndPoints(geojson);

        layers[id].startMarker
            .setLngLat(startEndPoint[0] as M.LngLatLike)
            .addTo(map);

        if (showInfoPopup) {
            const popup = createPopupFromTrail(trail);
            layers[id].startMarker.setPopup(popup);
        }

        layers[id].endMarker ??= new FontawesomeMarker(
            { icon: "fa fa-flag-checkered" },
            {},
        );
        layers[id].endMarker.setLngLat(startEndPoint[1] as M.LngLatLike);
        if (map.getZoom() > minZoom) {
            layers[id].endMarker.addTo(map);
        }
    }

    export function togglePopup(id: string, currentState?: boolean) {
        if (
            currentState !== undefined &&
            layers[id]?.startMarker?.getPopup().isOpen() != currentState
        ) {
            return;
        }
        layers[id]?.startMarker?.togglePopup();
    }

    function showWaypoints() {
        if (!map) {
            return;
        }
        for (const waypoint of trails[activeTrail]?.expand.waypoints ?? []) {
            const marker = createMarkerFromWaypoint(waypoint, onMarkerDragEnd);
            marker.addTo(map);
            markers.push(marker);
        }
    }

    function hideWaypoints() {
        if (!map) {
            return;
        }
        for (const m of markers) {
            m.remove();
        }
    }

    onMount(async () => {
        const initialState = {
            lng: 0,
            lat: 0,
            zoom: 1,
        };
        const ElevationProfileControl = (
            await import(
                "$lib/vendor/maplibre-elevation-profile/elevationprofile-control"
            )
        ).ElevationProfileControl;

        const mapStyles: { text: string; value: string; thumbnail?: string }[] =
            [
                ...(($page.data.settings as Settings)?.tilesets ?? []).map(
                    (t) => ({
                        text: t.name,
                        value: t.url,
                    }),
                ),
                {
                    text: "Open Street Maps",
                    value: "/styles/osm.json",
                    thumbnail: "https://tile.openstreetmap.org/1/0/0.png",
                },
                {
                    text: "Open Topo Maps",
                    value: "/styles/otm.json",
                    thumbnail: "https://tile.opentopomap.org/1/0/0.png",
                },
                {
                    text: "Carto Light",
                    value: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
                    thumbnail:
                        "https://basemaps.cartocdn.com/light_all/1/0/0@2x.png",
                },
                {
                    text: "Carto Dark",
                    value: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
                    thumbnail:
                        "https://basemaps.cartocdn.com/dark_all/1/0/0@2x.png",
                },
            ];
        let preferredMapStyleIndex = mapStyles.findIndex(
            (s) => s.text === localStorage.getItem("layer"),
        );

        if (preferredMapStyleIndex == -1) {
            preferredMapStyleIndex = 0;
        }

        const finalMapOptions: M.MapOptions = {
            ...{
                container: mapContainer,
                style:
                    mapStyles[preferredMapStyleIndex].value ??
                    mapStyles[0].value,
                center: [initialState.lng, initialState.lat],
                zoom: initialState.zoom,
            },
            ...mapOptions,
        };
        map = new M.Map(finalMapOptions);

        const elevationMarker = new FontawesomeMarker(
            {
                id: "elevation-marker",
                icon: "fa-regular fa-circle",
                fontSize: "xs",
                width: 4,
                backgroundColor: "bg-primary",
                fontColor: "white",
            },
            {},
        );
        elevationMarker.setLngLat([0, 0]).addTo(map);
        elevationMarker.setOpacity("0");

        let img = new Image(20, 20);
        img.onload = () => map!.addImage("direction-caret", img);
        img.src = directionCaret;

        const switcherControl = new StyleSwitcherControl({
            styles: mapStyles,
            onSwitch: (style) => {
                layers = {};
                map?.setStyle(style.value);
                localStorage.setItem("layer", style.text);
            },
            selectedIndex:
                preferredMapStyleIndex !== -1 ? preferredMapStyleIndex : 0,
        });
        map.addControl(
            new M.NavigationControl({ visualizePitch: showTerrain }),
        );
        map.addControl(
            new M.ScaleControl({
                maxWidth: 120,
                unit: $page.data.settings?.unit ?? "metric",
            }),
            "top-left",
        );

        if (showElevation) {
            epc = new ElevationProfileControl({
                visible: false,
                profileBackgroundColor:
                    $theme == "light" ? "#242734" : "#191b24",
                backgroundColor: "bg-menu-background/90",
                unit: $page.data.settings?.unit ?? "metric",
                profileLineWidth: 3,
                displayDistanceGrid: true,
                tooltipDisplayDPlus: false,
                zoom: false,
                container: elevationProfileContainer,
                onEnter: () => {
                    elevationMarker.setOpacity("1");
                },
                onLeave: () => {
                    elevationMarker.setOpacity("0");
                },
                onMove: (data) => {
                    elevationMarker.setLngLat(data.position as M.LngLatLike);
                },
            });
            map.addControl(epc);
        }
        if (showStyleSwitcher) {
            map.addControl(switcherControl);
        }

        if (showFullscreen) {
            map.addControl(
                new FullscreenControl(() => {
                    dispatch("fullscreen");
                }),
                "bottom-right",
            );
        }

        if (showTerrain) {
            map!.addControl(
                new M.TerrainControl({
                    source: "terrain",
                }),
            );
        }

        map.on("styledata", () => {
            trails.forEach((t, i) => {
                addTrailLayer(t, t.id ?? i.toString(), i, data?.at(i));
            });

            if (showTerrain) {
                try {
                    if (
                        $page.data.settings?.terrain?.terrain &&
                        !map?.getSource("terrain")
                    ) {
                        map!.addSource("terrain", {
                            type: "raster-dem",
                            url: $page.data.settings.terrain.terrain,
                        });
                    }
                    if (
                        $page.data.settings?.terrain?.hillshading &&
                        !map?.getSource("hillshading")
                    ) {
                        map!.addSource("hillshading", {
                            type: "raster-dem",
                            url: $page.data.settings.terrain.hillshading,
                        });
                        map!.addLayer({
                            id: "hillshading",
                            source: "terrain",
                            type: "hillshade",
                        });
                    }
                } catch (e) {}
            }
        });

        map.on("moveend", (e) => {
            dispatch("moveend", e.target);
        });

        map.on("zoom", (e) => {
            const zoom = e.target.getZoom();
            Object.values(layers).forEach((l) => {
                if (zoom > minZoom && map) {
                    l.endMarker?.addTo(map);
                } else {
                    l.endMarker?.remove();
                }
            });

            dispatch("zoom", e.target);
        });

        map.on("click", (e) => {
            dispatch("click", e);
        });

        map.on("load", () => {
            addCaretLayer(trails[activeTrail]?.id);
            dispatch("init", map);
        });

        showWaypoints();
    });

    onDestroy(() => {
        map?.remove();
    });
</script>

<div id="map" bind:this={mapContainer}></div>

<style>
    #map {
        width: 100%;
        height: 100%;
    }

    :global(.maplibregl-popup-content) {
        @apply bg-background rounded-md;
    }

    :global(.maplibregl-popup-close-button) {
        top: 4px;
        right: 8px;
    }
</style>
